package darknet.application.chat.impl;

import darknet.application.chat.impl.net.ChatMessageFactory;
import darknet.application.chat.spec.Channel;
import darknet.application.chat.spec.ChannelObserver;
import darknet.application.chat.spec.MessageObserver;
import darknet.application.chat.impl.net.message.BroadcastChannelMessage;
import darknet.application.chat.impl.net.message.BroadcastExitChannelMessage;
import darknet.application.chat.impl.net.message.BroadcastJoinChannelMessage;
import darknet.application.chat.impl.net.message.ChannelMessage;
import darknet.application.chat.impl.net.message.ExitChannelMessage;
import darknet.application.chat.impl.net.message.JoinChannelMessage;
import darknet.core.spec.net.NetworkManager;
import darknet.core.spec.net.Node;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Channel implementation.
 * This version if for channel hosted in a node.
 *
 * This channel can receive 3 messages :
 *
 * - Join message : A node has join this channel; 
 *                  We add it to the list of connected node
 *
 * - Channel message :  A connected node has sent a message;
 *                      Broadcast the message to all connected nodes.
 *
 * - Exit message : A connected node has exit the channel.
 * 
 * @author yoyo
 */
public class ChannelHostedImpl implements Channel {

    /** NetworkManager */
    transient private NetworkManager network = null;

    /** Chat message factory */
    transient private ChatMessageFactory factory = null;

    /** Channel hoster */
    private Node hoster = null;

    /** Channel name */
    private String channelName = null;

    /** Nodes connected to this channel. */
    private Set<Node> connectedNodes = null;

    /** Channel Observers */
    transient private Set<ChannelObserver> channelObserver = null;

    /** Chat application */
    transient private ChatApplicationImpl application = null;

    /**
     * Initialise a new channel impl.
     *
     * @param channelName
     * @param application
     */
    public ChannelHostedImpl(String channelName,
            ChatApplicationImpl application, MessageObserver messageObserver) {

        if (channelName == null || channelName.length() == 0) {
            throw new IllegalArgumentException("channelName is null");
        }
        this.channelName = channelName;

        if (application == null) {
            throw new IllegalArgumentException("application is null");
        }
        this.application = application;

        network = application.getNetworkManager();
        hoster = network.myNode();
        factory = ChatMessageFactory.getInstance(network);

        connectedNodes = new HashSet<Node>(10);
        channelObserver = new HashSet<ChannelObserver>(1);
    }

    @Override
    public void join() {}

    @Override
    public void speak(String message) throws IOException {
        
        newChannelMessage(factory.makeChannelMessage(
                message, hoster, channelName));

    }

    @Override
    public void exit() throws IOException {
        close();
    }

    @Override
    public void close() {

        for (Node to : connectedNodes) {

            // Dont send the close message to ourself.
            if (!to.getName().equals(hoster.getName())) {

                try {
                    network.send(factory.makeCloseChannelMessage(to, channelName));
                } catch (IOException ex) {
                    System.err.println(
                            "IOException on sending close channel message : "
                            + ex.getLocalizedMessage());
                }
            }
        }

        // Close the hosted channel
        application.closeHostedChannel(this);
    }

    @Override
    public Set<Node> getNodes() {
        
        Set<Node> nodes = new HashSet<Node>(connectedNodes);
        nodes.add(hoster);
        return nodes;
    }

    @Override
    public Node getHoster() {
        return hoster;
    }

    @Override
    public String getName() {
        return channelName;
    }

    @Override
    public void addChannelObserver(ChannelObserver toAdd) {
        if (toAdd == null) {
            throw new IllegalArgumentException("toAdd is null");
        }
        channelObserver.add(toAdd);
    }

    @Override
    public void removeChannelObserver(ChannelObserver toRemove) {
        if (toRemove == null) {
            throw new IllegalArgumentException("toRemove is null");
        }
        channelObserver.remove(toRemove);
    }

    @Override
    public List<ChannelObserver> getChannelObservers() {
        return new ArrayList<ChannelObserver>(channelObserver);
    }

    /**
     * Handle join channel message.
     *
     * @param event
     */
    void newJoinChannelMessage(JoinChannelMessage event) {

        // A node has joined the channel.
        connectedNodes.add(event.getFrom());


        // broadcast the event to listeners
        if (!event.getFrom().getName().equals(hoster.getName())) {

            for (ChannelObserver observer : channelObserver) {
                observer.newJoinChannelMessage(event);
            }
        }
        

        // broadcast the event to connected nodes
        BroadcastJoinChannelMessage bcast = null;
        for (Node node : connectedNodes) {

            if (!node.getName().equals(event.getFrom().getName())) {

                bcast = factory.makeBroadcastJoinChannelMessage(
                        node, channelName, event.getFrom());

                try {
                    network.send(bcast);
                } catch (IOException ex) {
                }
            }
        }

        
    }

    /**
     * Handle channel message.
     *
     * @param event
     */
    void newChannelMessage(ChannelMessage event) {

        // the channel is from a connected node ?
        if (!getNodes().contains(event.getFrom())) {
            return;
        }

        // broadcast the event to listeners
        if (!event.getFrom().getName().equals(hoster.getName())) {
            
            for (ChannelObserver observer : channelObserver) {

                observer.newBroadcastChannelMessage(
                        factory.makeBroadcastChannelMessage(
                        event.getText(), event.getTo(),
                        event.getChannelName(), event.getFrom()));

            }
        }

        // broadcast the event to connected nodes
        BroadcastChannelMessage bcastChanMessage = null;
        for (Node node : connectedNodes) {

            // Send the message to all nodes, except the original poster
            if (!node.getName().equals(event.getFrom().getName())) {

                bcastChanMessage = factory.makeBroadcastChannelMessage(
                        event.getText(), node, event.getChannelName(),
                        event.getFrom());
                try {
                    network.send(bcastChanMessage);
                } catch (IOException ex) {
                }
            }

        }
    }

    /**
     * Handle exit channel 
     *
     * @param event
     */
    void newExitChannelMessage(ExitChannelMessage event) {

         // A node has exit the channel.
        connectedNodes.remove(event.getFrom());

        // broadcast the event to listeners
        for (ChannelObserver observer : channelObserver) {
            observer.newExitChannelMessage(event);
        }
        
        // broadcast the event to connected nodes
        BroadcastExitChannelMessage bcast = null;
        for (Node node : connectedNodes) {
            bcast = factory.makeBroadcastExitChannelMessage(
                    node, channelName, event.getFrom());

            try {
                network.send(bcast);
            } catch (IOException ex) {}
        }
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof ChannelHostedImpl &&
                toString().equals(((ChannelHostedImpl) obj).toString());
    }

    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    @Override
    public String toString() {
        return channelName;
    }
}
